package fi.iki.joker.mobilogger.util;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;

import android.content.Context;
import android.util.Log;
import android.webkit.WebView;
import android.widget.ImageView;
import fi.iki.joker.mobilogger.MobiLoggerConstants;
import fi.iki.joker.mobilogger.exception.CaptchaChallengeException;
import fi.iki.joker.mobilogger.exception.GoogleClientLoginException;

public class GoogleClientLoginUtil {
	
	private final String CLIENT_LOGIN_URL_PREFIX = "http://www.google.com/accounts/";
	private final String TAG = "GoogleClientLoginUtil";
	private final String GOOGLE_CLIENTLOGIN_URL = "https://www.google.com/accounts/ClientLogin";
	private final String RESPONSE_TOKEN_URL = "Url";
	private final String RESPONSE_TOKEN_ERROR = "Error";
	private final String RESPONSE_TOKEN_CAPTCHATOKEN = "CaptchaToken";
	private final String RESPONSE_TOKEN_CAPTCHAURL = "CaptchaUrl";
	private final String RESPONSE_TOKEN_SID = "SID";
	private final String RESPONSE_TOKEN_LSID = "LSID";
	private final String RESPONSE_TOKEN_AUTH = "Auth";
	public final String ERROR_CODE_BAD_AUTHENTICATION = "BadAuthentication";
	public final String ERROR_CODE_NOT_VERIFIED = "NotVerified";
	public final String ERROR_CODE_TERMS_NOT_AGREED = "TermsNotAgreed";
	public final String ERROR_CODE_CAPTCHA_REQUIRED = "CaptchaRequired";
	public final String ERROR_CODE_UNKNOWN = "Unknown";
	public final String ERROR_CODE_ACCOUNT_DELETED = "AccountDeleted";
	public final String ERROR_CODE_ACCOUNT_DISABLED = "AccountDisabled";
	public final String ERROR_CODE_SERVICE_DISABLED = "ServiceDisabled";
	public final String ERROR_CODE_SERVICE_UNAVAILABLE = "ServiceUnavailable";
	
	private HttpClient client;
	
	/**
	 * Performs authentication against google clientLogin interface.
	 * Responds with either valid authentication token or an Exception.
	 * If google wants to have a captcha solved by the user, a CaptchaChallengeException is thrown,
	 * which can then be used to show the captcha to the user (see documentation of that exception).
	 * 
	 * In the case of GoogleClientLoginException, the caller should investigate the errorCode within
	 * the exception and compare it against the public error codes declared by the class if more action needs to be.
	 * taken because of that error code. For convenience, the exception message is something that you can directly show to user
	 * in most cases.
	 * 
	 * @param service The service identifier for the specific googleService you are trying to use
 	 * @param userName user name to used in authentication
	 * @param password password to be used in authentication
	 * @param captchaToken the captcha id, this is provided in the CaptchaChallengeException should one occur
	 * @param captchaAnswer user's answer to the captcha challenge, provide if needed (CaptchaChallengeException occured)
	 * @return The authorization token from GoogleClientLogin. Append this to the header.
	 * @throws CaptchaChallengeException
	 */
	
	public String performLogin(String service,String userName, String password,
			String captchaToken, String captchaAnswer) throws CaptchaChallengeException, GoogleClientLoginException {
		String res = null;
		int rescode = 0;
		HttpResponse response;
		HttpEntity responseEntity=null;
		HttpPost httpsPost = new HttpPost(GOOGLE_CLIENTLOGIN_URL);
		List <NameValuePair> pairs = new ArrayList <NameValuePair>();
		pairs.add(new BasicNameValuePair("accountType", "GOOGLE"));
		pairs.add(new BasicNameValuePair("Email", userName));
		pairs.add(new BasicNameValuePair("Passwd", password));
		pairs.add(new BasicNameValuePair("service", service));
		pairs.add(new BasicNameValuePair("source", MobiLoggerConstants.APPNAME));
		if(captchaToken != null && captchaAnswer != null) {
			pairs.add(new BasicNameValuePair("logintoken", captchaToken));
			pairs.add(new BasicNameValuePair("logincaptcha", captchaAnswer));
		}
		try {
			httpsPost.setEntity(new UrlEncodedFormEntity(pairs));
		} catch (UnsupportedEncodingException e1) {
			Log.e(TAG, "Unsupported encoding using while HttpsPost");
			throw new GoogleClientLoginException(e1);
		}
		try {
				if(client == null) {
					client = new DefaultHttpClient();
				}
				httpsPost.addHeader("Content-type", "application/x-www-form-urlencoded");
				response = client.execute(httpsPost);
				rescode = response.getStatusLine().getStatusCode();
				Log.d(TAG,"Response code: "+rescode);
				responseEntity = response.getEntity();
				if(responseEntity == null) {
					Log.d(TAG,"Response entity was null!");
					throw new HttpException("The response from post call was null!");
				} else {
					Log.d(TAG,"Response body len: "+responseEntity.getContentLength());
					res=StreamUtil.convertStreamToString(responseEntity.getContent());
					Log.d(TAG,"Response was: "+res);
				}
			} catch (HttpException he) {
				Log.e(TAG,"HTTPException during client comms:"+he.getMessage());
				throw new GoogleClientLoginException(he);
			} catch (IOException ie) {
				Log.e(TAG,"IOException during client comms:"+ie.getMessage());
				throw new GoogleClientLoginException(ie);
			} finally {
				Log.d(TAG,"Executed POST request to Google without exceptions.");
				try {
					if(responseEntity != null) {
						responseEntity.consumeContent();
					}
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
					Log.e(TAG,"IOException when consuming response entity content.");
					throw new GoogleClientLoginException(e);
				}
			}
		// if all went well HTTP OK
		if(rescode == java.net.HttpURLConnection.HTTP_OK) {
			Log.d(TAG,"HTTP status OK, finding AUTH String");
			String auth_token = getResponseToken(res, RESPONSE_TOKEN_AUTH);
			Log.d(TAG,"Received auth token: "+auth_token);
			return auth_token; 
		// If we got 403, access forbidden, then there may be many options why the
		// access was denied
		} else if (rescode == java.net.HttpURLConnection.HTTP_FORBIDDEN) {
			Log.d(TAG,"HTTP status is HTTP_FORBIDDEN. Continuing with error processing.");
			String errorCode = getResponseToken(res, RESPONSE_TOKEN_ERROR);
			if(errorCode == null) {
				String msg = "Cannot get error code after HTTP 403 (Access forbidden). Can't respond with anything...";
				Log.e(TAG,msg);
				GoogleClientLoginException ex = new GoogleClientLoginException(msg,errorCode);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_ACCOUNT_DELETED)) {
				String msg = "This account is deleted according to Google login service.";
				Log.i(TAG,msg);
				GoogleClientLoginException ex = new GoogleClientLoginException(msg,errorCode);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_ACCOUNT_DISABLED)) {
				String msg = "This account is disable according to Google login service.";
				Log.i(TAG,msg);
				GoogleClientLoginException ex = new GoogleClientLoginException(msg,errorCode);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_BAD_AUTHENTICATION)) {
				String msg = "Your authetication is rejected by the Google login service.";
				Log.i(TAG,msg);
				GoogleClientLoginException ex = new GoogleClientLoginException(msg,errorCode);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_CAPTCHA_REQUIRED)) {
				// This exception is thrown to signal the caller that Google Client Login
				// wants the user to respond to a CAPTCHA challenge. The caller should display the
				// CAPTHCA URL and recall this service with the same CAPTCHA token and and the CAPTCHA
				// response as parameters.
				Log.i(TAG, "Google login service requires CAPTCHA for access.");
				CaptchaChallengeException ex = new CaptchaChallengeException();
				String captchaTokenStr = getResponseToken(res,RESPONSE_TOKEN_CAPTCHATOKEN);
				String captchaUrlStr = getResponseToken(res,RESPONSE_TOKEN_CAPTCHAURL);
				ex.setCaptchaToken(captchaTokenStr);
				ex.setCaptchaUrl(captchaUrlStr);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_NOT_VERIFIED)) {
				String msg = "The captcha could not be verified. Re-enterin captcha challenge.";
				Log.i(TAG,msg);
				//TODO: check that the NOT VERIFIED is actually "CAPTCHA not verified" 
				//and not "USER not verified. I don't have currently access to the docs...
				//TODO: Verify how this actually works. If the captcha code is not verified,
				//does the client login protocol issue another CAPTCHA or should we continue with the
				//old one (new captcha would be nice since otherwise we need to store the old one
				//somewhere, which would not be nice.
				CaptchaChallengeException ex = new CaptchaChallengeException();
				String captchaTokenStr = getResponseToken(res,RESPONSE_TOKEN_CAPTCHATOKEN);
				String captchaUrlStr = getResponseToken(res,RESPONSE_TOKEN_CAPTCHAURL);
				ex.setCaptchaToken(captchaTokenStr);
				ex.setCaptchaUrl(captchaUrlStr);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_SERVICE_DISABLED)) {
				String msg = "The authentication service is currently disabled. Please try again later.";
				Log.i(TAG,msg);
				GoogleClientLoginException ex = new GoogleClientLoginException(msg,errorCode);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_SERVICE_UNAVAILABLE)) {
				String msg = "Google login service reports that it is currently disabled. Please try again later.";
				Log.i(TAG,msg);
				GoogleClientLoginException ex = new GoogleClientLoginException(msg,errorCode);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_TERMS_NOT_AGREED)) {
				String msg = "Google login service reports that you have not agreed to their terms of service. If you want to use this "+
				"functionality, you need to resolve this and retry.";
				Log.i(TAG,msg);
				GoogleClientLoginException ex = new GoogleClientLoginException(msg,errorCode);
				throw ex;
			} else if(errorCode.equals(ERROR_CODE_UNKNOWN)) {
				String msg = "Google login service responded wiht unknow return code ("+errorCode+"). ";
				Log.i(TAG,msg);
				GoogleClientLoginException ex = new GoogleClientLoginException(msg,errorCode);
				throw ex;
			}
			Log.e(TAG,"Falling of the authentication routine without a way to continue.");
			//Typically the null would signal failure, however since null authentication code is a valid answer it the
			//case that google throws us a CAPTCHA, we treat this falling out of the function without a way
			//to continue as a ClientLoginException. This situation is identified with null exception id code.
			throw new GoogleClientLoginException("Authentication terminated abnormally.",null);
		} else {
			//This is the scenario where we get some other HTTP code than OK or Access denied.
			//TODO: check what google documentation says about this situation.. are there other valid codes?
			Log.e(TAG,"Google Client Login responded with a return code this client can't handle. ("+rescode+")");
			throw new GoogleClientLoginException("Nonactionable return code: ("+rescode+")", null);			
		}
	}
	
	/**
	 * Given google ClientLogin wants a captcha to be solved, the webview provided by this
	 * method can be used on the interface layer to view the captcha graphic.
	 * The result view is only usable in the context of the caller
	 * @param caller the context of the caller.  
	 * @param e The exception that contains the CAPTCHA details.
	 * @return
	 */
	
	public WebView getCaptchaImage(Context caller, CaptchaChallengeException e) throws GoogleClientLoginException {
		if(caller == null || e == null) {
			Log.e(TAG, "Required parameter null when calling getCaptchaImage (context:"+caller+" exception:"+e+")");
		}
		String captchaUrl = e.getCaptchaUrl();
		URL url = null;
		try {
			url = new URL(CLIENT_LOGIN_URL_PREFIX+captchaUrl);
		} catch (MalformedURLException url_e) {
			Log.e(TAG,"Malformed URL while getting Captcha image.");
			throw new GoogleClientLoginException(url_e);
		}
		if(url != null) {
			WebView res = new WebView(caller);
			res.loadUrl(url.toString());
			return res;
		}
		Log.w(TAG,"Exiting method getCaptchaImage with null return value as fallback!");	
		return null;
	}
	
	/**
	 * Parses the authentication token from the authentication response.
	 * @param authResponse the response to parse
	 * @param tokenId, what is that we are looking for?
	 * @return String, the Auth=[x] string
	 */
	
	private String getResponseToken(String authResponse, String tokenId) throws GoogleClientLoginException {
		String matchPattern = "[\\=|\\s]";
		if(authResponse != null) {
			Log.d(TAG,"Response to be matched against pattern :"+matchPattern+" Looking for token: "+tokenId);
			String [] matches = authResponse.split(matchPattern);
			for(int i = 0; i < matches.length; i++) {
				Log.d(TAG,"Match #"+i+" is "+matches[i]);
				//the split should result in an array where name-value pairs are
				//followed by each other, ie name,value,name,value,...
				String searchedValue = null;
				try {
					if(matches[i].equals(tokenId)) {
						searchedValue = matches[i+1];
						return searchedValue;
					} 
				} catch (ArrayIndexOutOfBoundsException e) {
					Log.e(TAG,"Failed to get the value for response token "+tokenId+" because of split array out of bounds.");
					throw new GoogleClientLoginException(e);
				}
			}
		} else {
			Log.e(TAG,"NULL authResponse passed to getAuthToken!");
			return null;
		}
		Log.w(TAG,"Fallback null return executed.");
		return null;
	}
}
